import os
import time
from subprocess import Popen, PIPE
from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
from threading import Timer
from signal import SIGTERM, SIGKILL

class TimeLimitExceededError(Exception):
    pass

class UnknownCommandError(Exception):
    pass

class Config(SafeConfigParser):
    def __init__(self, config_file):
        SafeConfigParser.__init__(self, defaults={'time_limit': '30'})
        if hasattr(config_file, 'read'):
            self.readfp(config_file)
        else:
            self.read(config_file)

    def get_command(self, name):
        """Return command and limitations by it's name."""
        try:
            command = self.get(name, 'exec')
        except (NoSectionError, NoOptionError):
            raise UnknownCommandError(name)
        time_limit = self.getfloat(name, 'time_limit')
        return command, dict(time_limit=time_limit)


def invoke_command(command, time_limit=None):
    """Run a command in subprocess.

    Keyword arguments:

        `time_limit`
            limit the subprocess' running time, in seconds.

    Once the command is finished, return a dict contains the following items:

        `stdout`
            the output on the standard output of the command.
        `stderr`
            the output on the standard error of the command.
        `exit_code`
            the exit code of the command.
        `start_time`
            the startup timestamp of this launch
        `time_cost`
            the total time cost to run this command, in second

    """
    start_time = time.time()
    p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
    killed = [False]
    if time_limit:
        timer = Timer(time_limit, _kill, (p, killed))
        timer.setDaemon(True)
        timer.start()
    stdout, stderr = p.communicate()
    if killed[0]:
        raise TimeLimitExceededError()
    time_cost = time.time() - start_time
    return {'stdout': stdout,
            'stderr': stderr,
            'exit_code': p.returncode,
            'start_time': start_time,
            'time_cost': time_cost,
           }

def _kill(process, killed=[False]):
    # killed is used to indicate the caller whether the killing happened.
    if process.poll() is None:
        killed[0] = True
        # first try kill the process gracefully
        os.kill(process.pid, SIGTERM)
        # wait for 1 second for the process quit
        for i in xrange(10):
            time.sleep(0.1)
            if process.poll() is not None:
                break
        else:
            os.kill(process.pid, SIGKILL)
    else:
        killed[0] = False

def run_command(config, name):
    """Invoke a command according to the config.

    If success, return what invoke_command() returns.  If error, a dictionary
    with error message returned.
    
    """
    try:
        command, limit = config.get_command(name)
        return invoke_command(command, **limit)
    except Exception, e:
        return {'error': "%s: %s" % (e.__class__.__name__, e)}
